﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MapEditor;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace PFAGame
{
    internal class MapGenerationOutput
    {
        public Vector2 playerPosition = new Vector2(0, 0);
        public List<AMob> mobList = new List<AMob>();
    }

    class Map
    {
        // info debug
      //  #if DEBUG
        Texture2D tmpTex;
        public bool displayPathFinding = false;
      //  #endif

        public static Decor Decor { get { return AssetsMapManager.Singleton.CurrentDecor; } }

        //info pathfinding
        const int refreshFrequency = 50;
        int currentRefresh = 0;

        const int pathfindingAreaSize = 50;
        float[,] pathfindingLayer = new float[pathfindingAreaSize, pathfindingAreaSize];
        float[,] oldPathfindingLayer = new float[pathfindingAreaSize, pathfindingAreaSize];
        Vector2I pathfindingOffset = new Vector2I(0, 0);
        Vector2I oldPathfindingOffset = new Vector2I(0, 0);
        Vector2I deltaOffset = new Vector2I();

        public delegate void SeedGeneratedEventHandler(object sender, int seed);
        static public event SeedGeneratedEventHandler seedGenerated;

        public Vector2 Size
        {
            get { return new Vector2(squares.GetLength(1), squares.GetLength(0)); }
        }

        // info display
        Square[,]   squares;

        //info génération procedurale
        Random      random;
        int         seed;

        public int Seed
        {
            get
            {
                return seed;
            }
            set
            {
                this.seed = value;
            }
        }

        /// <summary>
        /// (re)crée la map.
        /// </summary>
        /// <param name="mapdata">donnée de map chargé depuis le XML</param>
        /// <returns>structure contenant la liste de mobs instancié pendant la génération de la map, la position du joueur, la seed, ...</returns>
        public MapGenerationOutput generateMap(MapData mapdata, bool keepSeed)
        {
            MapGenerationOutput mapGenerationOutput = new MapGenerationOutput();
            if (mapdata.type == 1)
            {
                //map en vue isométrique:

                // instanciation du random en conservant la seed (en cas de debug)
                if (keepSeed == false)
                {
                    random = new Random();
                    seed = random.Next();
                }
                if (GameplayScreen.Singleton.NetworkSessionP != null && GameplayScreen.Singleton.NetworkSessionP.IsHost == true)
                    seedGenerated(this, seed);
                random = new Random(seed);

                // génération du labyrinthe à partir des donnée de la map
                mazeSquare[,] mazeData = MazeGenerator.generateMaze(mapdata.generationParameters[0], mapdata.generationParameters[1], mapdata.generationParameters[2], mapdata.generationParameters[3], seed);

                // allocation de la map pouvant contenir le labyrinthe
                squares = new Square[mazeData.GetLength(0) * mapdata.sizeY, mazeData.GetLength(1) * mapdata.sizeX];

                // pour chaque case du labyrinthe, récupération d'un module correspondant au prérequis de la case puis copie du module dans la map
                for (int i = 0; i < mazeData.GetLength(0); i++)
                    for (int j = 0; j < mazeData.GetLength(1); j++)
                        copyModuleOnMap(i, j, mapdata, mapdata.getModule(mazeData[i, j], random), mapGenerationOutput);
            }
            else
            {
                // map en vue de plateforme:

                // instanciation du random en conservant la seed (en cas de debug)
                random = new Random();
                seed = random.Next();
                random = new Random(seed);

                // allocation de la map puis copie de l'unique module dedans
                squares = new Square[mapdata.sizeY, mapdata.sizeX];
                copyModuleOnMap(0, 0, mapdata, mapdata.getModule(), mapGenerationOutput);
            }
            return mapGenerationOutput;
        }

        /// <summary>
        /// copie un module dans la map et instancie les éléments dynamique présent.
        /// </summary>
        /// <param name="offsetY">position du module dans le labyrinthe sur Y</param>
        /// <param name="offsetX">position du module dans le labyrinthe sur X</param>
        /// <param name="mapData">donnée de la map, contenant la taille des modules</param>
        /// <param name="module">module à recopier sur la map</param>
        /// <param name="mapGenerationOutput">structure à remplir avec les éléments instancié</param>
        private void copyModuleOnMap(int offsetY, int offsetX, MapData mapData, Module module, MapGenerationOutput mapGenerationOutput)
        {
            // boucle sur les cases du module
            for (int i = 0; i < mapData.sizeY; i++)
                for (int j = 0; j < mapData.sizeX; j++)
                {
                    Vector2I worldPosition = new Vector2I(j + offsetX * mapData.sizeX, i + offsetY * mapData.sizeY);
                    //création de la case courante
                    Square s = new Square(-1, -1, -1, -1, -1);
                    squares[worldPosition.y, worldPosition.x] = s;

                    // recopie les donnée du module sur la case
                    s.backgroundId = (short)module.background[i, j];
                    s.foregroundId = (short)module.foreground[i, j];
                    s.staticId =     (short)module.staticLayer[i, j];
                    s.dynamicId =   (short)module.dynamicLayer[i, j];

                    // si un éléments dynamique doit être instancié à cette case (monstre, ...)
                    switch (s.dynamicId)
                    {
                        case 2: mapGenerationOutput.playerPosition = new Vector2(worldPosition.x + 0.5f, worldPosition.y + 0.5f); break;    // spawn du joueur
                        case 3:                                                                                                             // spawn de mob
                            int randNbr = random.Next(5);
                            if (randNbr <= 1)       // chance 2/5 eme
                                mapGenerationOutput.mobList.Add(new MeleeRobot(worldPosition.x + 0.5f, worldPosition.y + 0.5f));
                            else if (randNbr <= 3)  // chance 2/5 eme
                                mapGenerationOutput.mobList.Add(new RangedRobot(worldPosition.x + 0.5f, worldPosition.y + 0.5f));
                            else                    // chance 1/5 eme
                                mapGenerationOutput.mobList.Add(new Kamikaz(worldPosition.x + 0.5f, worldPosition.y + 0.5f));
                            break;
                        case 5:                                                                                                             // spawn de boss
                            randNbr = random.Next(5);
                            if (randNbr <= 1)       // chance 2/5 eme
                                mapGenerationOutput.mobList.Add(new MeleeBoss(worldPosition.x + 0.5f, worldPosition.y + 0.5f));
                            else if (randNbr <= 3)  // chance 2/5 eme
                                mapGenerationOutput.mobList.Add(new RangedBoss(worldPosition.x + 0.5f, worldPosition.y + 0.5f));
                            else                    // chance 1/5 eme
                                mapGenerationOutput.mobList.Add(new KamikazBoss(worldPosition.x + 0.5f, worldPosition.y + 0.5f));
                            break;
                        case 6: mapGenerationOutput.mobList.Add(new Queen(worldPosition.x + 0.5f, worldPosition.y + 0.5f)); break; // reine
                        case 7: mapGenerationOutput.mobList.Add(new Cavalier(worldPosition.x + 0.5f, worldPosition.y + 0.5f)); break; // chevalier
                        case 8: mapGenerationOutput.mobList.Add(new Bishop(worldPosition.x + 0.5f, worldPosition.y + 0.5f)); break; // fou
                        case 9: mapGenerationOutput.mobList.Add(new Tower(worldPosition.x + 0.5f, worldPosition.y + 0.5f)); break; // tour
                        default: break;
                       // TODO: completer la liste d'instanciation
                    }
                    if (GameplayScreen.Singleton.NetworkSessionP != null && !GameplayScreen.Singleton.NetworkSessionP.IsHost)
                        mapGenerationOutput.mobList.Clear();
                }
        }

        public void init()
        {
            tmpTex = new Texture2D(ScreenManager.Singleton.GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
            tmpTex.SetData(new[] { Color.White });
        }

        public void update(GameTime gameTime)
        {
            // actualisation du pathfinding:

            // refresh toute les "refreshFrequency" ms
            currentRefresh +=  gameTime.ElapsedGameTime.Milliseconds;
            if (currentRefresh > refreshFrequency)
                currentRefresh = 0;
            else
                return;

            // swap des layers
            float[,] tmpPathFindingLayer = oldPathfindingLayer;
            Vector2I tmpPathFindingOffset = oldPathfindingOffset;
            oldPathfindingOffset = pathfindingOffset;
            oldPathfindingLayer = pathfindingLayer;
            pathfindingOffset = tmpPathFindingOffset;
            pathfindingLayer = tmpPathFindingLayer;

            // calcul de la position du layer
            pathfindingOffset.x = (int)GameplayScreen.Singleton.CameraP.Position.X - pathfindingAreaSize / 2;
            pathfindingOffset.y = (int)GameplayScreen.Singleton.CameraP.Position.Y - pathfindingAreaSize / 2;
            deltaOffset.set(-oldPathfindingOffset.x + pathfindingOffset.x, -oldPathfindingOffset.y + pathfindingOffset.y);

            // parcours des cases
            for (int i = 0; i < pathfindingAreaSize; i++)
                for (int j = 0; j < pathfindingAreaSize; j++)
                {
                    if (i + deltaOffset.y >= 1 && i + deltaOffset.y < pathfindingAreaSize - 1 &&
                        j + deltaOffset.x >= 1 && j + deltaOffset.x < pathfindingAreaSize - 1)
                    {
                        // on choisi la valeur la plus élevé parmis les 4 cases alentour et la case local...
                        float aggroNorth = oldPathfindingLayer[i + deltaOffset.y + 1, j + deltaOffset.x];
                        float aggroSud = oldPathfindingLayer[i + deltaOffset.y - 1, j + deltaOffset.x];
                        float aggroEast = oldPathfindingLayer[i + deltaOffset.y, j + deltaOffset.x + 1];
                        float aggroWest = oldPathfindingLayer[i + deltaOffset.y, j + deltaOffset.x - 1];
                        float aggroCurrent = oldPathfindingLayer[i + deltaOffset.y, j + deltaOffset.x];
                        aggroNorth = (aggroNorth >  aggroSud ? aggroNorth : aggroSud);
                        aggroWest = (aggroWest > aggroEast ? aggroWest : aggroEast);
                        aggroNorth = (aggroNorth > aggroWest ? aggroNorth : aggroWest);
                        aggroNorth = (aggroNorth > aggroCurrent ? aggroNorth : aggroCurrent);

                        // ... puis on l'applique à la case courente diminué du coeficient de la case courante et de 2/3
                        pathfindingLayer[i, j] = aggroNorth * 2 / 3 * getCoeficient(j + pathfindingOffset.x, i + pathfindingOffset.y);
                    }
                    else
                        pathfindingLayer[i, j] = 0; // bord de layer, on peut l'arrondir à 0 pour éviter de traiter les exceptions
                }
            // add pathfinding local player 
            foreach (APlayer player in GameplayScreen.Singleton.Players)
            {
                if ((int)player.Position.X - pathfindingOffset.x >= 0 && (int)player.Position.X - pathfindingOffset.x < pathfindingAreaSize &&
                    (int)player.Position.Y - pathfindingOffset.y >= 0 && (int)player.Position.Y - pathfindingOffset.y < pathfindingAreaSize)
                    pathfindingLayer[(int)player.Position.Y - pathfindingOffset.y, (int)player.Position.X - pathfindingOffset.x] = APlayer.aggro;
            }
            // add pathfinding network player
            if (GameplayScreen.Singleton.NetworkSessionP != null && GameplayScreen.Singleton.NetworkSessionP.IsHost == true)
            {
                foreach (APlayer player in GameplayScreen.Singleton.DicoPlayer.Values)
                {
                    if ((int)player.Position.X - pathfindingOffset.x >= 0 && (int)player.Position.X - pathfindingOffset.x < pathfindingAreaSize &&
                        (int)player.Position.Y - pathfindingOffset.y >= 0 && (int)player.Position.Y - pathfindingOffset.y < pathfindingAreaSize)
                        pathfindingLayer[(int)player.Position.Y - pathfindingOffset.y, (int)player.Position.X - pathfindingOffset.x] = APlayer.aggro;
                }
            }
            // add pathfinding mobs
            foreach (AMob mob in GameplayScreen.Singleton.MobList)
            {
                if ((int)mob.Position.X - pathfindingOffset.x >= 0 && (int)mob.Position.X - pathfindingOffset.x < pathfindingAreaSize &&
                    (int)mob.Position.Y - pathfindingOffset.y >= 0 && (int)mob.Position.Y - pathfindingOffset.y < pathfindingAreaSize)
                    pathfindingLayer[(int)mob.Position.Y - pathfindingOffset.y, (int)mob.Position.X - pathfindingOffset.x] *= 0.7f;
            }
        }

         /// <summary>
         /// retourne un coeficient entre 0 et 1 en fonction de la case testé par le pathfinding
         /// </summary>
         /// <param name="x"> position sur X de la case à tester</param>
         /// <param name="y"> position sur y de la case à tester</param>
         /// <returns></returns>
        private float getCoeficient(int x, int y)
        {
            if (x >= 0 && x < squares.GetLength(1) &&
                y >= 0 && y < squares.GetLength(0))
                switch (squares[y, x].dynamicId)
                {
                    case 1: return 0;      // case bloquante
                    default: return 1;
                }
            return 1;
        }

        /// <summary>
        /// retourne un vecteur direction normalisé à partir du layer de pathfinding. appeler par les mobs.
        /// </summary>
        /// <param name="position">position du mob</param>
        /// <returns></returns>
        public Vector2 getDirectionFromPathfinding(Vector2 position)
        {
            int y = (int)position.Y - pathfindingOffset.y;
            int x = (int)position.X - pathfindingOffset.x;
            if (x >= 1 && x < pathfindingAreaSize - 1 &&
                y >= 1 && y < pathfindingAreaSize - 1)
            {
                Vector2 direction = new Vector2();
                direction.Y = pathfindingLayer[y + 1, x] - pathfindingLayer[y - 1, x];
                direction.X = pathfindingLayer[y, x + 1] - pathfindingLayer[y, x - 1];
                if (direction.X !=0 || direction.Y != 0)
                    direction.Normalize();
                return direction;
            }
            return new Vector2(0, 0);
        }

        /// <summary>
        /// dessine l'arriére plan: le background layer et le static layer
        /// </summary>
        public void drawBackground(GameTime gameTime, Camera camera)
        {
            //récupération de la taille du viewport
            int height = ScreenManager.Singleton.GraphicsDevice.Viewport.Height;
            int width = ScreenManager.Singleton.GraphicsDevice.Viewport.Width;

            //boucle sur les coordonnée des tiles à afficher
            for (int i = -1; i < (height / camera.ZoomLevel) + 2; i++)
                for (int j = -1; j < (width / camera.ZoomLevel) + 2; j++)
                {
                    // si la case est en dehors de la map, on continue
                    if (i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel < 0 || i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel >= squares.GetLength(0) ||
                        j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel < 0 || j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel >= squares.GetLength(1))
                        continue;

                    //calcul du rectangle d'affichage de la tile en cour
                    Rectangle rec = new Rectangle((int)((j - camera.Position.X + Math.Floor(camera.Position.X)) * camera.ZoomLevel),
                                                    (int)((i - camera.Position.Y + Math.Floor(camera.Position.Y)) * camera.ZoomLevel),
                                                    camera.ZoomLevel,
                                                    camera.ZoomLevel);

                    // recupération et affichage des 2 textures correspondant à la tile en cours, si elles existent
                    Texture2D tex = AssetsMapManager.Singleton.getTextureFromId(squares[i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel, j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel].backgroundId);
                    if (tex != null)
                        ScreenManager.Singleton.SpriteBatch.Draw(tex, rec, Color.White);
                    tex = AssetsMapManager.Singleton.getTextureFromId(squares[i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel, j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel].staticId);
                    if (tex != null)
                        ScreenManager.Singleton.SpriteBatch.Draw(tex, rec, Color.White);

                   // #if DEBUG
                    if (displayPathFinding)
                    {
                        int x = j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel - pathfindingOffset.x;
                        int y = i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel - pathfindingOffset.y;
                        if (x >= 0 && x < pathfindingAreaSize &&
                            y >= 0 && y < pathfindingAreaSize)
                        {
                            float value = pathfindingLayer[y, x] / 1000 * 255;
                            Color colorDebugPathFinding = new Color((int)value, (int)value, (int)value, (int)value);
                            ScreenManager.Singleton.SpriteBatch.Draw(tmpTex, rec, colorDebugPathFinding);
                        }
                    }
                  //  #endif
                }
        }

        /// <summary>
        /// dessine l'avant plan: le foreground layer
        /// </summary>
        public void drawForeground(GameTime gameTime, Camera camera)
        {
            //récupération de la taille du viewport
            int height = ScreenManager.Singleton.GraphicsDevice.Viewport.Height;
            int width = ScreenManager.Singleton.GraphicsDevice.Viewport.Width;

            //boucle sur les coordonnée des tiles à afficher
            for (int i = -1; i < (height / camera.ZoomLevel) + 1; i++)
                for (int j = -1; j < (width / camera.ZoomLevel) + 1; j++)
                {
                    // si la case est en dehors de la map, on continue
                    if (i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel < 0 || i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel >= squares.GetLength(0) ||
                        j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel < 0 || j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel >= squares.GetLength(1))
                        continue;

                    //calcul du rectangle d'affichage de la tile en cour
                    Rectangle rec = new Rectangle((int)((j - camera.Position.X + Math.Floor(camera.Position.X)) * camera.ZoomLevel), 
                                                    (int)((i - camera.Position.Y + Math.Floor(camera.Position.Y)) * camera.ZoomLevel),
                                                    camera.ZoomLevel,
                                                    camera.ZoomLevel);

                    // recupération et affichage de la texture correspondant à la tile en cour, si elle existe
                    Texture2D tex = AssetsMapManager.Singleton.getTextureFromId(squares[i + (int)camera.Position.Y - (int)camera.OffsetMiddleScreen.Y / camera.ZoomLevel, j + (int)camera.Position.X - (int)camera.OffsetMiddleScreen.X / camera.ZoomLevel].foregroundId);
                    if (tex != null)
                        ScreenManager.Singleton.SpriteBatch.Draw(tex, rec, Color.White);
                }
        }

        const float halfHotBox = 0.3f;

        /// <summary>
        /// test la collision avec la map
        /// </summary>
        /// <param name="position"> position testé</param>
        /// <returns> true si il y a collision</returns>
        internal bool checkCollision(Vector2 position)
        {
            // TODO: completer la liste de colision
            if (position.X - 0.5 >= 0 && position.X + 0.5 < squares.GetLength(1) &&
                position.Y - 0.5 >= 0 && position.Y + 0.5 < squares.GetLength(0))
            {
                // test les 4 coins de la hitbox
                switch (squares[(int)(position.Y - halfHotBox), (int)(position.X - halfHotBox)].dynamicId)
                {
                    case 4: GameplayScreen.Singleton.changeLevel = true; break; //cas trigger fin lvl
                    case 0: break;          // case traverssable
                    case 1: return true;    // case bloquante
                    case 2: break;          // spawn du joueur
                    case 3: break;          // spawn de mob
                }
                switch (squares[(int)(position.Y - halfHotBox), (int)(position.X + halfHotBox)].dynamicId)
                {
                    case 4: GameplayScreen.Singleton.changeLevel = true; break; //cas trigger fin lvl
                    case 0: break;          // case traverssable
                    case 1: return true;    // case bloquante
                    case 2: break;          // spawn du joueur
                    case 3: break;          // spawn de mob
                }
                switch (squares[(int)(position.Y + halfHotBox), (int)(position.X + halfHotBox)].dynamicId)
                {
                    case 4: GameplayScreen.Singleton.changeLevel = true; break; //cas trigger fin lvl
                    case 0: break;          // case traverssable
                    case 1: return true;    // case bloquante
                    case 2: break;          // spawn du joueur
                    case 3: break;          // spawn de mob
                }
                switch (squares[(int)(position.Y + halfHotBox), (int)(position.X - halfHotBox)].dynamicId)
                {
                    case 4: GameplayScreen.Singleton.changeLevel = true; break; //cas trigger fin lvl
                    case 0: break;          // case traverssable
                    case 1: return true;    // case bloquante
                    case 2: break;          // spawn du joueur
                    case 3: break;          // spawn de mob
                }
            }
            else
                return true;
            return false;
        }

        internal List<AMob> respawnMobs(int wave)
        {
            int temp = wave;

            //TODO générer une liste de spawner à l'instanciation pour éviter de reparcourir la map
            List<AMob> newList = new List<AMob>();
            for (int i = 0; i < squares.GetLength(0); i++)
                for (int j = 0; j < squares.GetLength(1); j++)
                {
                    temp = wave;
                    switch (squares[i, j].dynamicId)
                    {
                        case 3:                                                                                                            // spawn de mob
                            while (temp != 0)
                            {
                                int randNbr = random.Next(5);
                                if (randNbr <= 1)       // chance 2/5 eme
                                    newList.Add(new MeleeRobot(j + 0.5f, i + 0.5f));
                                else if (randNbr <= 3)  // chance 2/5 eme
                                    newList.Add(new RangedRobot(j + 0.5f, i + 0.5f));
                                else                    // chance 1/5 eme
                                    newList.Add(new Kamikaz(j + 0.5f, i + 0.5f));
                                temp--;
                            }
                            break;
                        case 5:                                                                                                             // spawn de boss
                            int randNbrr = random.Next(5);
                            if (randNbrr <= 1)       // chance 2/5 eme
                                newList.Add(new MeleeBoss(j + 0.5f, i + 0.5f));
                            else if (randNbrr <= 3)  // chance 2/5 eme
                                newList.Add(new RangedBoss(j + 0.5f, i + 0.5f));
                            else                    // chance 1/5 eme
                                newList.Add(new KamikazBoss(j + 0.5f, i + 0.5f));
                            break;
                        case 6: newList.Add(new Queen(j + 0.5f, i + 0.5f)); break; // reine
                        case 7: newList.Add(new MeleeBoss(j + 0.5f, i + 0.5f)); break; // chevalier
                        case 8: newList.Add(new Bishop(j + 0.5f, i + 0.5f)); break; // fou
                        case 9: newList.Add(new Tower(j + 0.5f, i + 0.5f)); break; // tour
                        default: break;
                        // TODO: completer la liste d'instanciation
                    }
                }
            return newList;
        }
    }
}
